package SSF.OS.TCP;

/*
 * tcpSocket.java
 * Original author: Hongbo Liu Nov 16 1999
 * Revisions: Andy Ogielski Fri May 18 2001
 */

import com.renesys.raceway.SSF.*;
import SSF.OS.*;
import java.util.*;
import java.lang.Thread;
import SSF.OS.Socket.*;
import SSF.Net.Util.*;

/** BSD-style TCP socket pseudoprotocol for implementing
 *  protocols and applications that use TCP transport.
 */
public class tcpSocket extends ProtocolSession implements socketAPI {


  /***************** listening socket variables *****************/

  // connection request queues
  ArrayList incomplt_sockets;
  ArrayList complt_sockets;
  int Qlimit = 5;

  Continuation accept_call_waiting = null;
  socketAPI[] accept_socket;

  /*********************   socket state   *********************/

  /** If the socket was created by a listening socket in
   *  response to a connection request, contains the reference
   *  to the listening socket. Used by tcpSession to inform
   *  the listening socket about processing of the pending request.
   *  Used by an incomplete socket if connection was dropped
   *  to get removed from the incomplete socket queue by its
   *  listening socket.
   */
  public tcpSocket listeningSocket = null;

  public int source_ip_addr = -1;
  public int dest_ip_addr;
  public int destination_port;
  public int source_port;

  /** Empty dataMessage for transferring virtual data. Can be
   *  safely recycled because it is not a payload of a TCP_Header.
   */
  dataMessage emptyMsg;

  short socket_state = 0;
  static final short SS_NBIO = 1;
  static final short SS_ISCONNECTED = 2;
  static final short SS_ISCONNECTING = 4;
  static final short SS_ISDISCONNECTING = 8;
  static final short SS_CANTRCVMORE = 16;

  boolean isListening = false;

  tcpSessionMaster tcpMaster;

  // protocol session that created this socket
  ProtocolSession userSession;

  // tcpSession bound to this tcpSocket
  tcpSession myTcpSession;

  // Socket.socketMaster session creating sockets in ProtocolGraph
  socketMaster socketSession;

  // Cached reference to a Continuation simulating the last
  // blocking call in a blocking socket. Will be called back when
  // the condition causing the blocking is changed.
  Continuation app_call_waiting = null;
  boolean appCallWaiting = false;

  boolean DEBUG;

  /************************* Constructors ************************/

  /** Creates an unconnected, unnamed TCP socket.
   *  It is recommended to use SSF.OS.Socket.socketMaster.socket() to
   *  create a socket.
   */
  public tcpSocket(ProtocolSession caller, socketMaster sm) {
    userSession  = caller;
    tcpMaster    = sm.tcpMaster;
    socketSession = sm;
    emptyMsg = new dataMessage(null, 0);
    DEBUG = sm.DEBUG;
  }
  
  /************************ general socket function ***********************/

  /** Socket diagnostics message formatting.
   */
  public void socketInfo(String str){
    System.err.println
       (tcpMaster.inGraph().now()/(double)SSF.Net.Net.seconds(1.0)
        + " tcpSocket host " + ((SSF.Net.Host)tcpMaster.inGraph()).nhi
        + " port " + source_port + " - " + str);
  }

  /** Bind the socket to a local port number and IP address. <P>
   *  A socket user acting as client need not care about local
   *  IP address, if it is left undefined then the IP protocol will
   *  set it to the address of the interface used to send the TCP/IP
   *  messages out. In contrast, a well-known local address and port
   *  must be bound to a server socket before it can listen for
   *  connection requests.
   */
  public void bind(int src_ip, int src_port){
    source_port = src_port;
    source_ip_addr = src_ip;
  }

  /** Bind a tcpSession to this socket.
   *  Callback from tcpSessionMaster in response to active or passive
   *  open request.
   */
  public void bindTcpSession(tcpSession tcpSess){
    myTcpSession = tcpSess;
  }

  /** Called by tcpSession.drop() when connection is dropped
   *  (drop() removes the tcpSession bound to the socket).
   *  If a blocking socket method is in progress, invokes
   *  the failure(errno) method of the Continuation that
   *  simulates the blocking.
   */
  public void failure(int error){
    if(DEBUG)
      socketInfo("failure: "+socketMaster.errorString(error));

    // if an incomplete connection was dropped in SYN_RECEIVED state,
    // or an established connection was dropped before being accepted,
    // remove the socket from incomplete socket queue.
    if(listeningSocket != null)
      listeningSocket.removeFromQueue(this);

    if (appCallWaiting){

      // do not need to clean up the tcpSession end of socket
      // because it has been removed from the tcpSessionMaster
      // registry by drop() and cannot receive any more data.

      appCallWaiting = false;
      app_call_waiting.failure(error);
    }
  }

  /** No-op, returns false. Required by ProtocolSession interface.
   */
  public boolean push(ProtocolMessage message,
                      ProtocolSession fromSession)
         throws ProtocolException{
    return false;
  }

  /******************** listening socket function ********************/

  /** Passive open a TCP session and listen on the port bound to the socket
   *  for incoming connection requests. <P>
   *  SSF.OS.TCP implements the pending request queue management in the BSD
   *  style: the incoming connection requests are rejected
   *  if the sum of the number of pending incomplete connections (i.e.
   *  before the TCP 3-way handshake is completed) and of the number of
   *  pending established connections (i.e. not yet accepted by server)
   *  is greater than 1.5*Qlimit.
   *
   * <P>Note that different operating systems implement the request queue
   * managment differently, for instance in Linux 2.2 the argument Qlimit
   * specifies the queue length for  completely  established  sockets
   * waiting  to  be  accepted, while the maximum length of the queue  for
   * incomplete sockets can be set using a separate method call.
   */
  public void  listen(int Qlimit){
    if(source_ip_addr < 0 || source_port < 0) {
      socketInfo("cannot listen(), "
                 +" socket not bound to a valid IP or port");
      System.exit(-1);
    }
    if(isListening) {
      socketInfo("Warning: listen() called already");
      System.exit(-1);
    }

    tcpMaster.openServer(this, source_ip_addr, source_port);
    isListening = true;
    this.Qlimit = Qlimit;
    incomplt_sockets = new ArrayList(5);
    complt_sockets = new ArrayList(5);
  }

  /** If this is a listening socket, returns a new socket created in
   *  response to received connection request. The new socket is put
   *  in the incomplete socket queue. Returns null if
   *  the pending request queue is larger than that specified by
   *  the Qlimit parameter in the listen() call that set up this
   *  listening socket.<P>
   *  Called by tcpSessionMaster when a connection request (SYN) arrives
   *  to a passively opened tcpSession.
   */
  public tcpSocket addSocket(int src_port, int dest_ip, int dest_port)
    throws ProtocolException{
    if(!isListening) {
      throw new ProtocolException("Socket: non-server socket cannot" + 
                                   " accept a connection!");
    }
    int qsize = incomplt_sockets.size() + complt_sockets.size();
    if(qsize > Qlimit*3/2){
      if(DEBUG)
        socketInfo("connection request queue length= "+qsize+
                   ", limit exceeded, connection rejected");
      return null;
    }
    tcpSocket sock = new tcpSocket(userSession, socketSession);
    sock.listeningSocket = this;
    if((socket_state & SS_NBIO) != 0)
      sock.socket_state |= (SS_NBIO | SS_ISCONNECTING);
    sock.destination_port = dest_port;
    sock.dest_ip_addr = dest_ip;
    incomplt_sockets.add(sock);
    if(DEBUG)
      socketInfo("add incomplete socket, incompl_queue= "+incomplt_sockets.size());
    return sock;
  }

  /** Informs the listening socket that a pending connection
   *  request has become an established connection ready to
   *  be accepted.
   */
  public void addConnection(tcpSocket sock) {
    int index = incomplt_sockets.indexOf(sock);
    sock.socket_state ^= SS_ISCONNECTING;
    sock.socket_state |= SS_ISCONNECTED;

    if(accept_call_waiting != null) {
      accept_socket[0] = (socketAPI)incomplt_sockets.get(index);
      incomplt_sockets.remove(index);
      Continuation accept_call = accept_call_waiting;
      accept_call_waiting = null;
      accept_call.success();
    } else {
      complt_sockets.add(incomplt_sockets.get(index));
      incomplt_sockets.remove(index);
    }
  }

  /** Accept a socket from the complete socket queue.<P>
   *  Continuation.success() callback is invoked when a complete
   *  socket is available. <P>
   *  Continuation.failure(errno) callback is invoked if an
   *  error condition is encountered.<P>
   *  Failure codes returned by the Continuation caller:
   *    <P>EWOULDBLOCK  Socket is non-blocking and no connections are pending.
   *    <P>EINVAL       Socket is not listening for connections (e.g., because
   *                    it was closed before any incoming connection request
   *                    was completed.
   */
    public void accept(socketAPI[] new_socket, Continuation caller)
         throws ProtocolException {
    if(!isListening) {
      socketInfo("error: Cannot accept without listen");
      throw new ProtocolException("");
    }

    if(complt_sockets.size() > 0){
      // if there is an established connections in complete socket
      // queue, place it in new_socket[0] and call back Continuation.

      new_socket[0] = (tcpSocket)complt_sockets.get(0);
      complt_sockets.remove(0);
      caller.success();

    } else {
      // if no established conections are pending and the socket
      // is non-blocking, return EWOULDBLOCK to Continuation

      if((socket_state & SS_NBIO) != 0)
        caller.failure(socketMaster.EWOULDBLOCK);

      else {
        // if no established conections are pending and the socket
        // is blocking, cache new_socket and Continuation caller until
        // an established connection arrives

        accept_call_waiting = caller;
        accept_socket = new_socket;
      }
    }
  }


  /** If TCP connection bound to socket sock is dropped,
   *  and sock is still in the listening socket's queue,
   *  remove sock from the queue.
   */
  public boolean removeFromQueue(tcpSocket sock) {
    if(!isListening) return true;

    int index = incomplt_sockets.indexOf(sock);
    if (index > -1) {
      incomplt_sockets.remove(index);
      if(DEBUG)
        socketInfo("removed from incomplete queue, queue size= "
                    +incomplt_sockets.size());
      return true;
    }

    index = complt_sockets.indexOf(sock);
    if (index > -1) {
      complt_sockets.remove(index);
      if(DEBUG)
        socketInfo("removed from complete queue, queue size= "
                   +complt_sockets.size());
      return true;
    }
    if(DEBUG)
        socketInfo("total queue size= "
                   +(incomplt_sockets.size()+complt_sockets.size()));
    return false;
  }

  /************************ Client socket function ***********************/

  /** Active open a TCP connection. If this socket is not bound
   *  to a local port number via bind(), automatically generate
   *  the local port number. <P>
   *  If this socket already has an open connection, callback
   *  the Continuation with error = EISCONN.
   *  If socket is set as a listening socket, callback the Continuation
   *  with error =
   *
   *  Errors:
   *    EISCONN   The socket is already connected.
   *    ECONNREFUSED  No one listening on the remote address.
   *    ETIMEDOUT  Timeout while attempting connection.
   *    EINPROGRESS  The  socket  is non-blocking and the connection
   *                  cannot be completed immediately.
   */
  public void connect(int remote_ip, int remote_port, Continuation caller)
         throws ProtocolException {
    if(DEBUG)
      socketInfo("connect to "+IP_s.IPtoString(remote_ip)+":"+remote_port);

    if((socket_state & SS_ISCONNECTED) != 0) {
      caller.failure(socketMaster.EISCONN);
      return;
    }
    app_call_waiting = caller;
    appCallWaiting = true;
    destination_port = remote_port;
    dest_ip_addr = remote_ip;
    source_port = socketSession.getPortNumber();
    tcpMaster.openConnect(this,
                   source_ip_addr, source_port,dest_ip_addr, destination_port);
    if((socket_state & SS_NBIO) != 0)
      caller.failure(socketMaster.EINPROGRESS);
  }   

  /** TCP calls it to indicate a connection has opened successfully */
  public void connected(){
    socket_state ^= SS_ISCONNECTING;
    socket_state |= SS_ISCONNECTED;
    if (!appCallWaiting){ 
      socketInfo("socket: no application is connecting");
      return;
    }
    appCallWaiting = false;
    app_call_waiting.success();
  }

  /********************* closing a connection *********************/

  /** Close a socket and release all its resources.
   *  On return this socket object is no longer usable as a communications
   *  endpoint.<P>
   *  If close a socket when a connection is established, initiate TCP disconnection
   *  procedure.<P>
   *  If close a passively listening socket, this method first aborts any pending
   *  connections on its connection request queue, returns the Continuation
   *  failure for any pending accept() call  with failure errno EBADF, and finally
   *  returns caller.success().
   */
  public void close (Continuation caller) throws ProtocolException {
    if (isListening) {
      // [BJP 2002.01.17]
      for (int i=incomplt_sockets.size()-1; i>=0; i--) {
        tcpSocket sock = (tcpSocket)incomplt_sockets.remove(i);
        sock.abort();
      }
      if (accept_call_waiting != null) {
        accept_call_waiting.failure(socketMaster.EBADF);
        accept_call_waiting = null;
      }
      app_call_waiting = null;
      tcpMaster.removeServer(source_ip_addr, source_port);
      caller.success();
    } else {
      app_call_waiting = caller;
      appCallWaiting = true;
      myTcpSession.close();
      socket_state |= SS_ISDISCONNECTING;
    }
  }

  /** Callback from tcpSession when an established connection
   *  is disconnected.
   */
  public void disconnected(){
    socket_state ^= SS_ISDISCONNECTING;
    if (!appCallWaiting){
      socketInfo("no application is disconnecting!");
      return;
    }
    appCallWaiting = false;
    // (at this point socket cannot be in queue)
    app_call_waiting.success();
  }

  /** User aborts the connection. Not used. */
  public void abort(){
    myTcpSession.sendReset();
    myTcpSession.drop(socketMaster.ECONNABORTED);
  }

  /************************ I/O Methods ***********************/

  /**  Write an application-level object to be transmitted.
   *   Only object reference is actually sent to the receiver, with nbytes
   *   value used to calculate packet sizes (as with virtual data).<P>
   *  Will invoke the caller.success() method after the sent nbytes bytes
   *  have been acknowledged; or will invoke the caller.failure(errno) method
   *  with appropriate error code if the connection has been dropped.<P>
   *  This is a simulated blocking method, and the write failure will
   *  be returned with error EBUSY if an attempt is made to write while
   *  the previous write's Continuation has not yet returned.
   *
   *   @param obj         Array of size 1, to pass a reference to an object
   *           to the message recipient.
   *
   *   @param nbytes      Nominal size in bytes. For a UDP socket, the object
   *           reference is passed in one datagram of size nbytes;
   *           for a TCP socket the object reference is sent in the first
   *           TCP segment, followed by zero or more segments padded with
   *           virtual data, so that the total size is nbytes.
   *           In either case, the receipient's matching read() Continuation
   *           returns success() only after nbytes were received, and then
   *           the reference to the transmitted object becomes available.
   */
  public void write(Object[] obj, int nbytes, Continuation caller){
    if(DEBUG)
      socketInfo("write object "+nbytes+"B");
    app_call_waiting = caller;
    if (obj[0] == null) {
       socketInfo("Warning: tcpSocket.write() null object - sent as virtual data.");
       write(nbytes, caller);
       return;
    }
    appCallWaiting = true;
    dataMessage dmsg = new dataMessage(obj[0], nbytes);
    myTcpSession.write(dmsg, caller);
  }

  /** not supported for tcp sockets */
  public void write(byte[] buf, int nbytes, Continuation caller) {
    socketInfo("Warning: tcpSocket does not implement byte write().");
  }

  /** Write nbytes bytes of virtual data. This will cause one or more TCP segments
   *  to be transmitted, with the total payload size equal to nbytes.
   *  Will invoke the caller.success() method after nbytes bytes of virtual data
   *  have been acknowledged; or will invoke the caller.failure(errno) method
   *  with appropriate error code if the TCP connection has been dropped.<P>
   *  This is a simulated blocking method, and the write failure will
   *  be returned with error EBUSY if an attempt is made to write while
   *  the previous write's Continuation has not yet returned.
   */
  public void write(int nbytes, Continuation caller){
    if(DEBUG)
      socketInfo("write data "+nbytes+"B");
    app_call_waiting = caller;
    appCallWaiting = true;
    emptyMsg.size = nbytes;
    myTcpSession.write(emptyMsg, caller);
  }

  /** not supported by tcp sockets */
  public void read(byte[] buf, int nbytes,  Continuation caller) {
    socketInfo("Warning: tcpSocket does not implement byte read().");
  }

  /** Read a reference to an object whose nominal size is nbytes.<P>
   *  Obviously, a pair of application-level protocols using write(obj,...)
   *  and read(obj,...) must agree on the interpretation of the transmitted
   *  object references. The object itself is NOT copied, and read()
   *  provides a reference to <I>the same object</I> that was used to
   *  send it via write(obj,...).<P>
   *  Will invoke the caller.success() method after nbytes bytes of virtual data
   *  have been received; or will invoke the caller.failure(errno) method
   *  with appropriate error code if the TCP connection has been dropped.<P>
   *  This is a simulated blocking method, and the read failure will
   *  be returned with error EBUSY if an attempt is made to read while
   *  the previous read's Continuation has not yet returned.
   */
  public void read(Object[] obj, int nbytes,  Continuation caller){
    if(DEBUG)
      socketInfo("read object "+nbytes+"B");
    app_call_waiting = caller;
    appCallWaiting = true;
    myTcpSession.read(obj, nbytes, caller);
  }

  /** Read nbytes of virtual data.<P>
   *  Will invoke the caller.success() method after nbytes bytes of virtual data
   *  have been received; or will invoke the caller.failure(errno) method
   *  with appropriate error code if the TCP connection has been dropped.<P>
   *  This is a simulated blocking method, and the read failure will
   *  be returned with error EBUSY if an attempt is made to read while
   *  the previous read's Continuation has not yet returned.
   */
  public void read(int nbytes, Continuation caller){
    if(DEBUG)
      socketInfo("read data "+nbytes+"B");
    app_call_waiting = caller;
    appCallWaiting = true;
    myTcpSession.read(null, nbytes, caller);
  }
}
